using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Threading;
using System.Windows.Threading;
using RxHeat.Common;
using RxHeat.Threading;
using RxHeat.Tracing;

namespace RxHeat.Wpf
{
    /// <summary>
    ///     RxHeat WPF Observable helper extension methods
    /// </summary>
    public static class ObservableExtensions
    {
        /// <summary>
        ///     Schedules an action to be executed on the dispatcher. using the passed <see cref="DispatcherPriority" />
        /// </summary>
        /// <param name="dispatcherScheduler">The dispatcher scheduler.</param>
        /// <param name="action">Action to be executed.</param>
        /// <param name="dispatcherPriority">The dispatcher priority.</param>
        /// <exception cref="System.ArgumentNullException">action</exception>
        public static void Schedule(this DispatcherScheduler dispatcherScheduler, Action action,
                                    DispatcherPriority dispatcherPriority)
        {
            if (action == null)
                throw new ArgumentNullException("action");

            dispatcherScheduler.Dispatcher.BeginInvoke(action, dispatcherPriority);
        }

        /// <summary>
        ///     Invokes the specified action synchronously on the dispatcher scheduler.
        ///     <remarks>
        ///         Checks if calling context is running within the UI thread, using <see cref="Dispatcher.CheckAccess" />
        ///     </remarks>
        /// </summary>
        /// <param name="dispatcherScheduler">The dispatcher scheduler.</param>
        /// <param name="action">The action.</param>
        /// <exception cref="System.ArgumentNullException">action</exception>
        public static void Invoke(this DispatcherScheduler dispatcherScheduler, Action action)
        {
            if (action == null)
                throw new ArgumentNullException("action");

            if (!dispatcherScheduler.Dispatcher.CheckAccess())
                dispatcherScheduler.Dispatcher.Invoke(action);
            else
                action();
        }

        /// <summary>
        ///     Schedules the action on a the dispatcher using with a <see cref="DispatcherPriority.Background" />
        /// </summary>
        /// <param name="dispatcherScheduler">The dispatcher scheduler.</param>
        /// <param name="action">The action.</param>
        /// <exception cref="System.ArgumentNullException">action</exception>
        public static void ScheduleBackgroundPriority(this DispatcherScheduler dispatcherScheduler, Action action)
        {
            if (action == null)
                throw new ArgumentNullException("action");

            dispatcherScheduler.Dispatcher.BeginInvoke(action, DispatcherPriority.Background);
        }

        #region ObservableCollection extensions

        /// <summary>
        ///     Uses the specified query to reset and fill the passed <see cref="ObservableCollection{T}" /> asynchronously.
        /// </summary>
        /// <remarks>
        ///     Use with caution. consumers should handle synchronization (insure that no other code will modify the
        ///     <see
        ///         cref="ObservableCollection{T}" />
        ///     )
        /// </remarks>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="contextEntity">The context entity.</param>
        /// <param name="collectionToRefresh">The collection to reset and refresh from the query.</param>
        /// <param name="oDataTimeOut">
        ///     A Timeout value, at which the function will notify of failure <see cref="Observable.Return{TResult}(TResult)" />
        /// </param>
        /// <param name="subscriptionScheduler">The scheduler where the query will be subscribed on.</param>
        /// <param name="observableCollectionScheduler">
        ///     The scheduler on which the <see cref="ObservableCollection{T}" /> will be modified.
        ///     The default if null is passed would be the <see cref="DispatcherScheduler.Current" />
        /// </param>
        /// <returns>
        ///     Will return an observable to notify of successful completion <b>true</b> or failure <b>false</b>
        /// </returns>
        public static IObservable<bool> ToObservableCollection<TEntity>(this IQueryable<TEntity> contextEntity,
                                                                        ObservableCollection<TEntity>
                                                                            collectionToRefresh,
                                                                        TimeSpan oDataTimeOut,
                                                                        IScheduler subscriptionScheduler = null,
                                                                        IScheduler observableCollectionScheduler = null)
        {
            try
            {
                return Observable.Defer(
                    () => Observable.Create<AysncResult<IList<TEntity>>>(observer =>
                        {
                            IList<TEntity> result = new List<TEntity>();
                            IDisposable internalDisposable = null;
                            string tracingKey = typeof (TEntity).DescribeType() + " Stream";

                            internalDisposable = contextEntity
                                .ToObservable(subscriptionScheduler ?? Scheduler.Default)
                                .TraceObservable(() => tracingKey, internalDisposable)
                                .Subscribe(result.Add,
                                           ex =>
                                               {
                                                   internalDisposable.TraceOnError(tracingKey, ex);
                                                   observer.OnNext(new AysncResult<IList<TEntity>>(ex));
                                                   observer.OnCompleted();
                                               },
                                           () =>
                                               {
                                                   internalDisposable.TraceOnCompleted(tracingKey);
                                                   observer.OnNext(new AysncResult<IList<TEntity>>(result));
                                                   observer.OnCompleted();
                                               });
                            internalDisposable.TraceSubscription(tracingKey);
                            return internalDisposable;
                        })
                    )
                                 .Timeout(oDataTimeOut, ThreadingSettings.TimeOutScheduler)
                                 .ObserveOn(observableCollectionScheduler ?? DispatcherScheduler.Current)
                                 .Do(x =>
                                     {
                                         if (!x.IsFaulted)
                                         {
                                             collectionToRefresh.Clear();
                                             x.Result.ToList().ForEach(collectionToRefresh.Add);
                                         }
                                     })
                                 .Select(x => !x.IsFaulted);
            }
            catch (Exception ex)
            {
                TraceManager.GetTracer("RxHeat.Wpf")
                            .Error(ex,
                                   new TraceDetails(
                                       string.Empty,
                                       Thread.CurrentThread.ToTraceString(),
                                       () => "Error while generating ObservableCollection from " + contextEntity));
                return Observable.Return(false);
            }
        }

        /// <summary>
        ///     Converts a collection that implements the <see cref="INotifyCollectionChanged" /> to a live Observable source. For example
        ///     <see cref="ObservableCollection{T}" />
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="observableCollection">The observable collection.</param>
        /// <param name="startWithInitialValues">
        ///     if set to <c>true</c> if you need a cold observable ending with hot updates [start with initial values].
        /// </param>
        /// <returns></returns>
        public static IObservable<NotifyCollectionChangedEventArgs> ToLiveObservable<T>(
            this ObservableCollection<T> observableCollection, bool startWithInitialValues = false)
        {
            IObservable<NotifyCollectionChangedEventArgs> changeStream = Observable
                .FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
                    handler => (sender, args) => handler(sender, args),
                    x => observableCollection.CollectionChanged += x,
                    x => observableCollection.CollectionChanged -= x)
                .Select(e => e.EventArgs);

            IObservable<NotifyCollectionChangedEventArgs> initialValues =
                Observable.Return(
                    new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, observableCollection));

            return startWithInitialValues ? initialValues.Concat(changeStream) : changeStream;
        }

        /// <summary>
        ///     Monitors when an <see cref="ObservableCollection{T}" /> is cleared either
        ///     through the <see cref="Collection{T}.Clear" /> function or through removing all items.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="collection">The collection.</param>
        /// <returns></returns>
        public static IObservable<bool> MonitorClearState<T>(this T collection)
            where T : INotifyCollectionChanged
        {
            return Observable.FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
                handler => (sender, args) => handler(sender, args),
                x => collection.CollectionChanged += x,
                x => collection.CollectionChanged -= x)
                             .Select(e => e.EventArgs)
                             .Select(x => x.Action == NotifyCollectionChangedAction.Reset ||
                                          (collection is ICollection && x.Action == NotifyCollectionChangedAction.Remove &&
                                           (((ICollection) collection).Count == 1 ||
                                            ((ICollection) collection).Count == 0)));
        }

        #endregion

        #region INotifyPropertyChanged Helpers

        /// <summary>
        ///     Converts an object that implements the <see cref="INotifyPropertyChanged" /> to an <see cref="Observable" /> of
        ///     <see
        ///         cref="PropertyChangedEventArgs" />
        /// </summary>
        /// <typeparam name="TNotifier">Monitored object type</typeparam>
        /// <param name="notifier">The Monitored object instance.</param>
        /// <returns></returns>
        public static IObservable<EventPattern<PropertyChangedEventArgs>> ObservePropertyChanged<TNotifier>(
            this TNotifier notifier)
            where TNotifier : INotifyPropertyChanged
        {
            IObservable<EventPattern<PropertyChangedEventArgs>> propertyChanged = Observable
                .FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
                    handler => (sender, args) => handler(sender, args),
                    x => notifier.PropertyChanged += x,
                    x => notifier.PropertyChanged -= x);

            return propertyChanged;
        }

        /// <summary>
        ///     Converts an object that implements the <see cref="INotifyPropertyChanged" /> to an <see cref="Observable" />
        ///     of values using the supplied <paramref name="propertyAccessor" />
        /// </summary>
        /// <typeparam name="TNotifier">Monitored object type</typeparam>
        /// <typeparam name="TProperty">The type of the property monitored</typeparam>
        /// <param name="notifier">The Monitored object instance.</param>
        /// <param name="propertyAccessor">An expression that will return the property to be monitored</param>
        /// <param name="startWithCurrent">
        ///     if set to <c>true</c> [start with current].
        /// </param>
        /// <returns></returns>
        /// <example>
        ///     <code>
        /// ViewModel.ObservePropertyChanged(x =&gt; x.IsChanged, true)
        /// .DistinctUntilChanged()
        /// .Subscribe(ic =&gt; Console.WriteLine(ic))
        ///   </code>
        /// </example>
        public static IObservable<TProperty> ObservePropertyChanged<TNotifier, TProperty>(
            this TNotifier notifier,
            Expression<Func<TNotifier, TProperty>> propertyAccessor,
            bool startWithCurrent = false)
            where TNotifier : INotifyPropertyChanged
        {
            // Parse the expression to find the correct property name.
            var member = (MemberExpression) propertyAccessor.Body;
            string name = member.Member.Name;

            // Compile the expression so we can run it to read the property value.
            Func<TNotifier, TProperty> reader = propertyAccessor.Compile();

            IObservable<EventPattern<PropertyChangedEventArgs>> propertyChanged = Observable
                .FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
                    handler => (sender, args) => handler(sender, args),
                    x => notifier.PropertyChanged += x,
                    x => notifier.PropertyChanged -= x);

            // Filter the events to the correct property name, then select the value of the property from the notifier.
            IObservable<TProperty> newValues = from p in propertyChanged
                                               where p.EventArgs.PropertyName == name
                                               select reader(notifier);

            // If the caller wants the current value as well as future ones, use Defer() so that the current value is read when the subscription
            // is added, rather than right now. Otherwise just return the above observable.
            return startWithCurrent
                       ? Observable.Defer(() => Observable.Return(reader(notifier)).Concat(newValues))
                       : newValues;
        }

        /// <summary>
        ///     Monitors changes from an <see cref="INotifyPropertyChanged" /> object, and produces a stream of values based on
        ///     the <paramref name="propertyAccessor" /> expression specified.
        /// </summary>
        /// <typeparam name="TNotifier">The type of the notifier.</typeparam>
        /// <typeparam name="TProperty">The type of the property.</typeparam>
        /// <param name="notifier">The notifier.</param>
        /// <param name="propertyAccessor">The property accessor.</param>
        /// <param name="startWithCurrent">
        ///     if set to <c>true</c> [start with current].
        /// </param>
        /// <returns></returns>
        public static IObservable<Tuple<TProperty, TProperty>> ObservePropertyChangedValueChanges<TNotifier, TProperty>(
            this TNotifier notifier,
            Expression<Func<TNotifier, TProperty>> propertyAccessor,
            bool startWithCurrent = false)
            where TNotifier : INotifyPropertyChanged
        {
            return ObservePropertyChangedValueChanges(notifier, propertyAccessor, null, startWithCurrent);
        }

        /// <summary>
        ///     Monitors changes from an <see cref="INotifyPropertyChanged" /> object, and produces a stream of values based on
        ///     the <paramref name="propertyAccessor" /> expression specified.
        /// </summary>
        /// <typeparam name="TNotifier">
        ///     The type of the monitored object. The object that implements <see cref="INotifyPropertyChanged" />.
        /// </typeparam>
        /// <typeparam name="TProperty">The type of the values returned in the stream.</typeparam>
        /// <param name="notifier">The Monitored object.</param>
        /// <param name="propertyAccessor">The property accessor expression. Used on the Monitored object to extract the value changes.</param>
        /// <param name="distinctOnlySelector">
        ///     When this parameter is supplied, only distinct value changes are funneled through.
        ///     This expression will determine the Key for value change comparison. Internally it will be used by
        ///     <see
        ///         cref="Observable.DistinctUntilChanged{TSource,TKey}(System.IObservable{TSource},System.Func{TSource,TKey},System.Collections.Generic.IEqualityComparer{TKey})" />
        /// </param>
        /// <param name="startWithCurrent">
        ///     if set to <c>true</c> [start with current].
        /// </param>
        /// <returns>
        ///     An Observable of <see cref="System.Tuple{T1}" /> objects that contain the old value in <b>Item1</b> and the new changed value in <b>Item2</b>
        /// </returns>
        public static IObservable<Tuple<TProperty, TProperty>> ObservePropertyChangedValueChanges<TNotifier, TProperty>(
            this TNotifier notifier,
            Expression<Func<TNotifier, TProperty>> propertyAccessor,
            Func<TProperty, object> distinctOnlySelector,
            bool startWithCurrent = false)
            where TNotifier : INotifyPropertyChanged
        {
            // Compile the expression so we can run it to read the property value.
            Func<TNotifier, TProperty> accessorReader = propertyAccessor.Compile();
            IObservable<TProperty> newValues = ObservePropertyChanged(notifier, propertyAccessor);
            if (startWithCurrent)
            {
                IObservable<TProperty> capturedNewValues = newValues;
                newValues = Observable.Defer(() => Observable.Return(accessorReader(notifier))
                                                             .Concat(capturedNewValues));
            }

            IObservable<Tuple<TProperty, TProperty>> resultObservable =
                Observable.Create<Tuple<TProperty, TProperty>>(obs =>
                    {
                        Tuple<TProperty, TProperty> oldNew = null;
                        IObservable<TProperty> values = newValues;

                        if (distinctOnlySelector != null)
                        {
                            values = values.DistinctUntilChanged(distinctOnlySelector);
                        }

                        return values
                            .Subscribe(v =>
                                {
                                    if (oldNew == null)
                                    {
                                        oldNew = Tuple.Create(default(TProperty), v);
                                    }
                                    else
                                    {
                                        oldNew = Tuple.Create(oldNew.Item2, v);
                                        obs.OnNext(oldNew);
                                    }
                                },
                                       obs.OnError,
                                       obs.OnCompleted);
                    });

            return resultObservable;
        }

        #endregion
    }
}